package com.bfly.mall.wechat.minipro.service;

import com.alibaba.fastjson.JSONObject;
import com.bfly.common.json.JsonUtil;
import com.bfly.common.util.FileUtil;
import com.bfly.core.base.entity.RemoteResponse;
import com.bfly.core.cache.EhCacheUtil;
import com.bfly.core.cache.SysConfigCacheUtil;
import com.bfly.core.config.ResourceConfigure;
import com.bfly.core.enums.SysError;
import com.bfly.core.exception.ServiceResponseException;
import com.bfly.mall.system.dto.WXMiniProConfigDTO;
import com.bfly.mall.wechat.common.WeChatAPi;
import com.bfly.mall.wechat.common.WeChatAssert;
import com.bfly.mall.wechat.common.WeChatHttpUtil;
import com.bfly.mall.wechat.minipro.dto.WeChatLoginResult;
import com.bfly.mall.wechat.minipro.dto.message.WeChatSubscribeMessage;
import com.wechat.pay.contrib.apache.httpclient.auth.PrivateKeySigner;
import com.wechat.pay.contrib.apache.httpclient.auth.Signer;
import com.wechat.pay.contrib.apache.httpclient.util.PemUtil;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.message.BasicNameValuePair;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Field;
import java.nio.file.Paths;
import java.security.AlgorithmParameters;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * 微信小程序API接口
 *
 * @author 长沙商脉科技有限公司
 * @date 2020/5/9 11:40
 */
public class WeChatMiniProAPi {

    private static Logger logger = LoggerFactory.getLogger(WeChatMiniProAPi.class);

    /**
     * 登录凭证校验。通过 wx.login 接口获得临时登录凭证 code 后传到开发者服务器调用此接口完成登录流程
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/5/9 11:42
     */
    public static final String LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    /**
     * JSAPI/小程序下单API
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/11/30 17:56
     */
    public static final String ORDER_SUBMIT_JSAPI_V3 = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";

    /**
     * 该接口提供所有微信支付订单的查询，商户可以通过查询订单接口主动查询订单状态，完成下一步的业务逻辑。
     * 需要调用查询接口的情况：
     * ◆ 当商户后台、网络、服务器等出现异常，商户系统最终未接收到支付通知；
     * ◆ 调用支付接口后，返回系统错误或未知交易状态情况；
     * ◆ 调用刷卡支付API，返回USERPAYING的状态；
     * ◆ 调用关单或撤销接口API之前，需确认支付状态；
     * 请求URL： https://api.mch.weixin.qq.com/v3/pay/transactions/id/{transaction_id}
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/5/11 15:16
     */
    public static final String ORDER_QUERY_V3 = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}";

    /**
     * 以下情况需要调用关单接口：商户订单支付失败需要生成新单号重新发起支付，要对原订单号调用关单，避免重复支付；
     * 系统下单后，用户支付超时，系统退出不再受理，避免用户继续，请调用关单接口。
     * 注意：订单生成后不能马上调用关单接口，最短调用时间间隔为5分钟。
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/5/11 18:54
     */
    public static final String ORDER_CLOSE_V3 = "https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{out_trade_no}/close";

    /**
     * 当交易发生之后一段时间内，由于买家或者卖家的原因需要退款时，卖家可以通过退款接口将支付款退还给买家，微信支付将在收到退款请求并且验证成功之后，按照退款规则将支付款按原路退到买家帐号上。
     * 注意：
     * 1、交易时间超过一年的订单无法提交退款
     * 2、微信支付退款支持单笔交易分多次退款，多次退款需要提交原支付订单的商户订单号和设置不同的退款单号。申请退款总金额不能超过订单金额。 一笔退款失败后重新提交，请不要更换退款单号，请使用原商户退款单号
     * 3、请求频率限制：150qps，即每秒钟正常的申请退款请求次数不超过150次,错误或无效请求频率限制：6qps，即每秒钟异常或错误的退款申请请求不超过6次
     * 4、每个支付订单的部分退款次数不能超过50次
     * 5、如果同一个用户有多笔退款，建议分不同批次进行退款，避免并发退款导致退款失败
     * 6、申请退款接口的返回仅代表业务的受理情况，具体退款是否成功，需要通过退款查询接口获取结果。
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/11 11:03
     */
    public static final String ORDER_REFUND_V1 = "https://api.mch.weixin.qq.com/secapi/pay/refund";

    /**
     * 提交退款申请后，通过调用该接口查询退款状态。退款有一定延时，用零钱支付的退款20分钟内到账，银行卡支付的退款3个工作日后重新查询退款状态。
     * 注意：如果单个支付订单部分退款次数超过20次请使用退款单号查询.当一个订单部分退款超过10笔后，商户用微信订单号或商户订单号调退款查询API查询退款时，默认返回前10笔和total_refund_count（订单总退款次数）。商户需要查询同一订单下超过10笔的退款单时，可传入订单号及offset来查询，微信支付会返回offset及后面的10笔，以此类推。当商户传入的offset超过total_refund_count，则系统会返回报错PARAM_ERROR。
     * 举例：
     * 一笔订单下的退款单有36笔，当商户想查询第25笔时，可传入订单号及offset=24，微信支付平台会返回第25笔到第35笔的退款单信息，或商户可直接传入退款单号查询退款
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/11 14:04
     */
    public static final String ORDER_REFUND_QUERY_V1 = "https://api.mch.weixin.qq.com/pay/refundquery";

    /**
     * 获取小程序码，适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/6/9 14:12
     */
    public static final String WX_A_CODE_UNLIMITED = "https://api.weixin.qq.com/wxa/getwxacodeunlimit";

    /**
     * 获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制。
     *
     * @author andy_hulibo@163.com
     * @date 2020/12/20 14:30
     */
    public static final String WX_A_CODE = "https://api.weixin.qq.com/wxa/getwxacode";

    /**
     * 获取小程序二维码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     *
     * @author andy_hulibo@163.com
     * @date 2020/12/21 12:05
     */
    public static final String WX_QR_CODE = "https://api.weixin.qq.com/cgi-bin/wxaapp/createwxaqrcode";

    /**
     * 小程序的订阅消息 POST
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/13 11:52
     */
    public static final String SUBSCRIBE_MESSAGE = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token={ACCESS_TOKEN}";

    /**
     * 微信支付V1 Api签名
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/11 12:04
     */
    public static String getWeChatPayV1Sign(Map<String, String> param, String apiKey) {
        Map<String, String> treeMap = new TreeMap<>(param);
        String sign = "";
        for (String key : treeMap.keySet()) {
            sign += key.concat("=").concat(treeMap.get(key)).concat("&");
        }
        sign = sign.concat("key=" + apiKey);
        sign = DigestUtils.md5Hex(sign);
        return sign.toUpperCase();
    }

    /**
     * 微信支付V3 Api签名
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/11 12:03
     */
    public static String getWeChatPayV3Sign(Map<String, String> param, String serialNumber, String privateKeyFilePath) {
        StringBuilder str = new StringBuilder();
        param.forEach((key, value) -> str.append(value).append("\n"));
        PrivateKey privateKey = getPrivateKey(privateKeyFilePath);
        PrivateKeySigner signer = new PrivateKeySigner(serialNumber, privateKey);
        Signer.SignatureResult result = signer.sign(str.toString().getBytes());
        final Field[] fields = result.getClass().getDeclaredFields();
        String sign = null;
        for (Field field : fields) {
            if (field.getName().equals("sign")) {
                try {
                    field.setAccessible(true);
                    sign = field.get(result).toString();
                    break;
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return sign;
    }

    /**
     * 获得商户私钥
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/2 17:58
     */
    public static PrivateKey getPrivateKey(String privateKeyFilePath) {
        PrivateKey privateKey;
        try (InputStream inputStream = FileUtils.openInputStream(new File(privateKeyFilePath))) {
            privateKey = PemUtil.loadPrivateKey(inputStream);
        } catch (Exception e) {
            throw new RuntimeException(e.getMessage());
        }
        return privateKey;
    }

    /**
     * 解密微信小程序返回的加密数据
     *
     * @param data       加密的数据
     * @param sessionKey 小程序访问服务器头部带的标识
     * @param iv         小程序获得的iv值
     * @author 长沙商脉科技有限公司
     * @date 2020/12/10 21:03
     */
    public static String decryptMiniProData(String data, String sessionKey, String iv) {
        sessionKey = EhCacheUtil.getData(EhCacheUtil.APPLET_SESSION_KEY_CACHE, sessionKey, String.class);
        //被加密的数据
        byte[] dataByte = Base64.decodeBase64(data);
        //加密秘钥
        byte[] keyByte = Base64.decodeBase64(sessionKey);
        //偏移量
        byte[] ivByte = Base64.decodeBase64(iv);
        try {
            Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
            SecretKeySpec spec = new SecretKeySpec(keyByte, "AES");
            AlgorithmParameters parameters = AlgorithmParameters.getInstance("AES");
            parameters.init(new IvParameterSpec(ivByte));
            cipher.init(Cipher.DECRYPT_MODE, spec, parameters);// 初始化
            byte[] resultByte = cipher.doFinal(dataByte);
            String result = new String(resultByte, "UTF-8");
            return result;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 获得临时登录凭证 code换取用户的openid和session_key
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/11/12 15:04
     */
    public static WeChatLoginResult code2Session(String code) {
        WXMiniProConfigDTO wxAppConfig = SysConfigCacheUtil.getWxMiniProConfig();
        RemoteResponse response;
        String url = WeChatMiniProAPi.LOGIN.concat("?appid=" + wxAppConfig.getAppId())
                .concat("&secret=" + wxAppConfig.getAppSecret())
                .concat("&js_code=" + code).concat("&grant_type=authorization_code");
        HttpGet get = new HttpGet(url);
        try {
            response = WeChatHttpUtil.weChatHttpRequest(get);
        } catch (Exception e) {
            throw new ServiceResponseException(SysError.REMOTE_ERROR, e.getMessage());
        }
        WeChatAssert.checkResponse(response);

        WeChatLoginResult loginResult = JsonUtil.parse(response.getBody(), WeChatLoginResult.class);
        return loginResult;
    }

    /**
     * 获取小程序全局唯一后台接口调用凭据
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/11/12 15:07
     */
    private static String getAccessToken() {
        WXMiniProConfigDTO wxAppConfig = SysConfigCacheUtil.getWxMiniProConfig();
        String accessToken = WeChatAPi.getAccessToken(wxAppConfig.getAppId(), wxAppConfig.getAppSecret());
        return accessToken;
    }

    /**
     * 生成无限制的小程序码
     * 适用于需要的码数量极多的业务场景。通过该接口生成的小程序码，永久有效，数量暂无限制
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/11/12 15:07
     */
    public static String getWxaCodeUnLimited(Map<String, Object> params) {
        RemoteResponse response;
        try {
            String url = WeChatMiniProAPi.WX_A_CODE_UNLIMITED.concat("?access_token=" + getAccessToken());
            HttpPost post = new HttpPost(url);
            if (params != null) {
                List<BasicNameValuePair> list = new ArrayList<>();
                for (String key : params.keySet()) {
                    list.add(new BasicNameValuePair(key, params.get(key).toString()));
                }
                UrlEncodedFormEntity entity;
                try {
                    entity = new UrlEncodedFormEntity(list);
                } catch (UnsupportedEncodingException e) {
                    throw new ServiceResponseException(SysError.PARAM_ERROR, "参数编码异常,请检查参数!");
                }
                post.setEntity(entity);
            }
            response = WeChatHttpUtil.weChatHttpRequest(post);
        } catch (Exception e) {
            throw new ServiceResponseException(SysError.REMOTE_ERROR, e.getMessage());
        }
        WeChatAssert.checkResponse(response);
        File file = FileUtil.base64ToImage(response.getBody());
        if (file != null) {
            String path = ResourceConfigure.getConfig().getTempRootPath();
            String fileName = response.hashCode() + FileUtil.getFileSuffix(file.getPath());
            return getImgUrl(file, path, fileName);
        }
        throw new ServiceResponseException(SysError.ERROR, "生成图片出错!");
    }

    /**
     * 获取小程序二维码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     *
     * @author andy_hulibo@163.com
     * @date 2020/12/21 12:04
     */
    public static String createQRCode(String path) {
        RemoteResponse response;
        try {
            String url = WeChatMiniProAPi.WX_QR_CODE.concat("?access_token=" + getAccessToken());
            response = getRemoteResponse(path, url);
        } catch (Exception e) {
            throw new ServiceResponseException(SysError.REMOTE_ERROR, e.getMessage());
        }
        WeChatAssert.checkResponse(response);
        File file = FileUtil.base64ToImage(response.getBody());
        if (file != null) {
            path = ResourceConfigure.getConfig().getImageRootPath();
            String name = SysConfigCacheUtil.getWxMiniProConfig().getName();
            name = StringUtils.isNotBlank(name) ? name : "";
            name = name + "-qrcode";
            return getImgUrl(path, file, name);
        }
        throw new ServiceResponseException(SysError.ERROR, "生成小程序二维码出错!");
    }

    /**
     * 获取小程序码，适用于需要的码数量较少的业务场景。通过该接口生成的小程序码，永久有效，有数量限制
     *
     * @author andy_hulibo@163.com
     * @date 2020/12/20 13:30
     */
    public static String getWxACode(String path) {
        RemoteResponse response;
        try {
            String url = WeChatMiniProAPi.WX_A_CODE.concat("?access_token=" + getAccessToken());
            response = getRemoteResponse(path, url);
        } catch (Exception e) {
            throw new ServiceResponseException(SysError.REMOTE_ERROR, e.getMessage());
        }
        WeChatAssert.checkResponse(response);
        File file = FileUtil.base64ToImage(response.getBody());
        if (file != null) {
            path = ResourceConfigure.getConfig().getImageRootPath();
            String name = SysConfigCacheUtil.getWxMiniProConfig().getName();
            name = StringUtils.isNotBlank(name) ? name : "";
            name = name + "-acode";
            return getImgUrl(path, file, name);
        }
        throw new ServiceResponseException(SysError.ERROR, "生成小程序码出错!");
    }

    private static RemoteResponse getRemoteResponse(String path, String url) throws UnsupportedEncodingException {
        RemoteResponse response;
        HttpPost post = new HttpPost(url);
        JSONObject json = new JSONObject();
        json.put("path", path);
        json.put("width", 430);
        StringEntity entity = new StringEntity(json.toJSONString());
        post.setEntity(entity);
        response = WeChatHttpUtil.weChatHttpRequest(post);
        return response;
    }

    private static String getImgUrl(String path, File file, String name) {
        String fileName = name + FileUtil.getFileSuffix(file.getPath());
        return getImgUrl(file, path, fileName);
    }

    private static String getImgUrl(File file, String path, String fileName) {
        File targetFile = new File(Paths.get(path, fileName).toUri());
        if (!targetFile.exists()) {
            try {
                FileUtils.moveFile(file, targetFile);
            } catch (IOException e) {
                throw new ServiceResponseException(SysError.ERROR, e.getMessage());
            }
        }
        path = Paths.get(ResourceConfigure.getConfig().getResourceRootPath()).relativize(Paths.get(targetFile.getPath())).toString();
        path = "/" + FilenameUtils.separatorsToUnix(path);
        return ResourceConfigure.getResourceHttpUrl(path);
    }

    /**
     * 发送订阅消息
     *
     * @author 长沙商脉科技有限公司
     * @date 2020/12/14 21:08
     */
    public static void sendSubscribeMessage(WeChatSubscribeMessage message) {
        String token = getAccessToken();
        String url = WeChatMiniProAPi.SUBSCRIBE_MESSAGE.replace("{ACCESS_TOKEN}", token);

        HttpPost post = new HttpPost(url);
        StringEntity entity = new StringEntity(message.toString(), "UTF-8");
        post.setEntity(entity);
        RemoteResponse response = WeChatHttpUtil.weChatHttpRequest(post);
        if (!response.isSuccessful()) {
            try {
                WeChatAssert.checkResponse(response);
            } catch (Exception e) {
                logger.warn("订阅消息发送失败:" + e.getMessage());
            }
        }
    }
}